/** @module GUI */
import {
  document
} from "../../DHTML"
import Controller from './Controller';
import BooleanController from './BooleanController';
import ColorController from './ColorController';
import FunctionController from './FunctionController';
import NumberController from './NumberController';
import OptionController from './OptionController';
import StringController from './StringController';

export default class GUI {

  /**
   * Creates a panel that holds controllers.
   * @example
   * new GUI();
   * new GUI( { container: document.getElementById( 'custom' ) } );
   *
   * @param {object} [options]
   * @param {boolean} [options.autoPlace=true]
   * Adds the GUI to `document.body` and fixes it to the top right of the page.
   *
   * @param {HTMLElement} [options.container]
   * Adds the GUI to this DOM element. Overrides `autoPlace`.
   *
   * @param {number} [options.width=245]
   * Width of the GUI in pixels, usually set when name labels become too long. Note that you can make
   * name labels wider in CSS with `.lil‑gui { ‑‑name‑width: 55% }`
   *
   * @param {string} [options.title=Controls]
   * Name to display in the title bar.
   *
   * @param {boolean} [options.injectStyles=true]
   * Injects the default stylesheet into the page if this is the first GUI.
   * Pass `false` to use your own stylesheet.
   *
   * @param {number} [options.touchStyles=true]
   * Makes controllers larger on touch devices. Pass `false` to disable touch styles.
   *
   * @param {GUI} [options.parent]
   * Adds this GUI as a child in another GUI. Usually this is done for you by `addFolder()`.
   *
   */
  get PATH() {
    return Controller.findPATH(this)
  }
  constructor(UC, {
    parent,
    autoPlace = parent === undefined,
    container,
    width = 320,
    title = 'Controls'
  } = {}) {
    this.parent = parent;
    this.children = [];

    this.isGUI = true
    this._title = title;
    this.UC = UC
    if (parent != null) {   

      this.parent.children.push(this);
      this.parent.folders.push(this);


      // Stop the constructor early, everything onward only applies to root GUI's
   ///////////////////
      var children = this._findData(`${parent.PATH}children`)
      children.push({
        type: "GUI",
        show: true,
        title:title,
        children: []
      })
      var data = this.UC.data
      data[`${parent.PATH}children`] = children
      this.UC.setData(data)
    }
    ////////////////////////////////////////////
    /**
     * The GUI containing this folder, or `undefined` if this is the root GUI.
     * @type {GUI}
     */

    /**
     * The top level GUI containing this folder, or `this` if this is the root GUI.
     * @type {GUI}
     */
    this.root = parent ? parent.root : this;

    /**
     * The list of controllers and folders contained by this GUI.
     * @type {Array<GUI|Controller>}
     */

    /**
     * The list of controllers contained by this GUI.
     * @type {Array<Controller>}
     */
    this.controllers = [];

    /**
     * The list of folders contained by this GUI.
     * @type {Array<GUI>}
     */
    this.folders = [];

    /**
     * Used to determine if the GUI is closed. Use `gui.open()` or `gui.close()` to change this.
     * @type {boolean}
     */
    this._closed = false;

    /**
     * Used to determine if the GUI is hidden. Use `gui.show()` or `gui.hide()` to change this.
     * @type {boolean}
     */
    this._hidden = false;

    /**
     * The outermost container element.
     * @type {HTMLElement}
     */
    this.domElement = document.createElement('div');
    this.domElement.classList.add('lil-gui');

    /**
     * The DOM element that contains the title.
     * @type {HTMLElement}
     */
    this.$title = document.createElement('div');
    this.$title.classList.add('title');
    this.$title.setAttribute('role', 'button');
    this.$title.setAttribute('aria-expanded', true);
    this.$title.setAttribute('tabindex', 0);

    this.$title.addEventListener('click', () => this.openAnimated(this._closed));
    this.$title.addEventListener('keydown', e => {
      if (e.code === 'Enter' || e.code === 'Space') {
        e.preventDefault();
        this.$title.click();
      }
    });

    // enables :active pseudo class on mobile
    this.$title.addEventListener('touchstart', () => {}, {
      passive: true
    });

    /**
     * The DOM element that contains children.
     * @type {HTMLElement}
     */
    this.$children = document.createElement('div');
    this.$children.classList.add('children');

    this.domElement.appendChild(this.$title);
    this.domElement.appendChild(this.$children);

    this.title(title);

    if (this.parent) {

      this.parent.$children.appendChild(this.domElement);
      return;

    }

    this.domElement.classList.add('root');

    if (container) {

      container.appendChild(this.domElement);

    } else if (autoPlace) {

      this.domElement.classList.add('autoPlace');
      document.body.appendChild(this.domElement);

    }

    if (width) {
      //this.domElement.style.setProperty('--width', width + 'px');
      this.UC.setData({
        width
      })
    }

    // Don't fire global key events while typing in the GUI:
    this.domElement.addEventListener('keydown', e => e.stopPropagation());
    this.domElement.addEventListener('keyup', e => e.stopPropagation());

  }

  /**
   * Adds a controller to the GUI, inferring controller type using the `typeof` operator.
   * @example
   * gui.add( object, 'property' );
   * gui.add( object, 'number', 0, 100, 1 );
   * gui.add( object, 'options', [ 1, 2, 3 ] );
   *
   * @param {object} object The object the controller will modify.
   * @param {string} property Name of the property to control.
   * @param {number|object|Array} [$1] Minimum value for number controllers, or the set of
   * selectable values for a dropdown.
   * @param {number} [max] Maximum value for number controllers.
   * @param {number} [step] Step value for number controllers.
   * @returns {Controller}
   */
  add(object, property, $1, max, step) {

    if (Object($1) === $1) {
    
      return new OptionController(this, object, property, $1);

    }

    const initialValue = object[property];
    switch (typeof initialValue) {

      case 'number':

        return new NumberController(this, object, property, $1, max, step);

      case 'boolean':

        return new BooleanController(this, object, property);

      case 'string':

        return new StringController(this, object, property);

      case 'function':
        return new FunctionController(this, object, property);
      default:
        throw new Error("[GUI] ?????????"+initialValue+" "+typeof initialValue)
    }

    // eslint-disable-next-line no-console
    console.error(`gui.add failed
	property:`, property, `
	object:`, object, `
	value:`, initialValue);

  }

  /**
   * Adds a color controller to the GUI.
   * @example
   * params = {
   * 	cssColor: '#ff00ff',
   * 	rgbColor: { r: 0, g: 0.2, b: 0.4 },
   * 	customRange: [ 0, 127, 255 ],
   * };
   *
   * gui.addColor( params, 'cssColor' );
   * gui.addColor( params, 'rgbColor' );
   * gui.addColor( params, 'customRange', 255 );
   *
   * @param {object} object The object the controller will modify.
   * @param {string} property Name of the property to control.
   * @param {number} rgbScale Maximum value for a color channel when using an RGB color. You may
   * need to set this to 255 if your colors are too bright.
   * @returns {Controller}
   */
  addColor(object, property, rgbScale = 1) {
    return new ColorController(this, object, property, rgbScale);
  }

  /**
   * Adds a folder to the GUI, which is just another GUI. This method returns
   * the nested GUI so you can add controllers to it.
   * @example
   * const folder = gui.addFolder( 'Position' );
   * folder.add( position, 'x' );
   * folder.add( position, 'y' );
   * folder.add( position, 'z' );
   *
   * @param {string} title Name to display in the folder's title bar.
   * @returns {GUI}
   */
  addFolder(title) {
    return new GUI(this.UC, {
      parent: this,
      title
    });
  }

  /**
   * Recalls values that were saved with `gui.save()`.
   * @param {object} obj
   * @param {boolean} recursive Pass false to exclude folders descending from this GUI.
   * @returns {this}
   */
  load(obj, recursive = true) {

    if (obj.controllers) {

      this.controllers.forEach(c => {

        if (c instanceof FunctionController) return;

        if (c._name in obj.controllers) {
          c.load(obj.controllers[c._name]);
        }

      });

    }

    if (recursive && obj.folders) {

      this.folders.forEach(f => {

        if (f._title in obj.folders) {
          f.load(obj.folders[f._title]);
        }

      });

    }

    return this;

  }

  /**
   * Returns an object mapping controller names to values. The object can be passed to `gui.load()` to
   * recall these values.
   * @example
   * {
   * 	controllers: {
   * 		prop1: 1,
   * 		prop2: 'value',
   * 		...
   * 	},
   * 	folders: {
   * 		folderName1: { controllers, folders },
   * 		folderName2: { controllers, folders }
   * 		...
   * 	}
   * }
   *
   * @param {boolean} recursive Pass false to exclude folders descending from this GUI.
   * @returns {object}
   */
  save(recursive = true) {

    const obj = {
      controllers: {},
      folders: {}
    };

    this.controllers.forEach(c => {

      if (c instanceof FunctionController) return;

      if (c._name in obj.controllers) {
        throw new Error(`Cannot save GUI with duplicate property "${c._name}"`);
      }

      obj.controllers[c._name] = c.save();

    });

    if (recursive) {

      this.folders.forEach(f => {

        if (f._title in obj.folders) {
          throw new Error(`Cannot save GUI with duplicate folder "${f._title}"`);
        }

        obj.folders[f._title] = f.save();

      });

    }

    return obj;

  }

  /**
   * Opens a GUI or folder. GUI and folders are open by default.
   * @param {boolean} open Pass false to close
   * @returns {this}
   * @example
   * gui.open(); // open
   * gui.open( false ); // close
   * gui.open( gui._closed ); // toggle
   */
  open(open = true) {

    this._closed = !open;

    this.$title.setAttribute('aria-expanded', !this._closed);
    this.domElement.classList.toggle('closed', this._closed);

    return this;

  }

  /**
   * Closes the GUI.
   * @returns {this}
   */
  close() {
    return this.open(false);
  }

  /**
   * Shows the GUI after it's been hidden.
   * @param {boolean} show
   * @returns {this}
   * @example
   * gui.show();
   * gui.show( false ); // hide
   * gui.show( gui._hidden ); // toggle
   */
  show(show = true) {

    this._hidden = !show;

    this.domElement.style.display = this._hidden ? 'none' : '';
    this._render("show", show)

    return this;

  }

  /**
   * Hides the GUI.
   * @returns {this}
   */
  hide() {
    return this.show(false);
  }

  openAnimated(open = true) {

    // set state immediately
    this._closed = !open;

    this.$title.setAttribute('aria-expanded', !this._closed);

    // wait for next frame to measure $children
    requestAnimationFrame(() => {

      // explicitly set initial height for transition
      const initialHeight = this.$children.clientHeight;
      this.$children.style.height = initialHeight + 'px';

      this.domElement.classList.add('transition');

      const onTransitionEnd = e => {
        if (e.target !== this.$children) return;
        this.$children.style.height = '';
        this.domElement.classList.remove('transition');
        this.$children.removeEventListener('transitionend', onTransitionEnd);
      };

      this.$children.addEventListener('transitionend', onTransitionEnd);

      // todo: this is wrong if children's scrollHeight makes for a gui taller than maxHeight
      const targetHeight = !open ? 0 : this.$children.scrollHeight;

      this.domElement.classList.toggle('closed', !open);

      requestAnimationFrame(() => {
        this.$children.style.height = targetHeight + 'px';
      });

    });

    return this;

  }

  /**
   * Change the title of this GUI.
   * @param {string} title
   * @returns {this}
   */
  title(title) {
    /**
     * Current title of the GUI. Use `gui.title( 'Title' )` to modify this value.
     * @type {string}
     */
    this._title = title;
    this.$title.innerHTML = title;

    this._render("title", title)
    return this;
  }

  /**
   * Resets all controllers to their initial values.
   * @param {boolean} recursive Pass false to exclude folders descending from this GUI.
   * @returns {this}
   */
  reset(recursive = true) {
    const controllers = recursive ? this.controllersRecursive() : this.controllers;
    controllers.forEach(c => c.reset());
    return this;
  }

  /**
   * Pass a function to be called whenever a controller in this GUI changes.
   * @param {function({object:object, property:string, value:any, controller:Controller})} callback
   * @returns {this}
   * @example
   * gui.onChange( event => {
   * 	event.object     // object that was modified
   * 	event.property   // string, name of property
   * 	event.value      // new value of controller
   * 	event.controller // controller that was modified
   * } );
   */
  onChange(callback) {
    /**
     * Used to access the function bound to `onChange` events. Don't modify this value
     * directly. Use the `gui.onChange( callback )` method instead.
     * @type {Function}
     */
    this._onChange = callback;
    return this;
  }

  _callOnChange(controller) {

    if (this.parent) {
      this.parent._callOnChange(controller);
    }

    if (this._onChange !== undefined) {
      this._onChange.call(this, {
        object: controller.object,
        property: controller.property,
        value: controller.getValue(),
        controller
      });
    }
  }

  /**
   * Pass a function to be called whenever a controller in this GUI has finished changing.
   * @param {function({object:object, property:string, value:any, controller:Controller})} callback
   * @returns {this}
   * @example
   * gui.onFinishChange( event => {
   * 	event.object     // object that was modified
   * 	event.property   // string, name of property
   * 	event.value      // new value of controller
   * 	event.controller // controller that was modified
   * } );
   */
  onFinishChange(callback) {
    /**
     * Used to access the function bound to `onFinishChange` events. Don't modify this value
     * directly. Use the `gui.onFinishChange( callback )` method instead.
     * @type {Function}
     */
    this._onFinishChange = callback;
    return this;
  }

  _callOnFinishChange(controller) {

    if (this.parent) {
      this.parent._callOnFinishChange(controller);
    }

    if (this._onFinishChange !== undefined) {
      this._onFinishChange.call(this, {
        object: controller.object,
        property: controller.property,
        value: controller.getValue(),
        controller
      });
    }
  }

  /**
   * Destroys all DOM elements and event listeners associated with this GUI
   */
  destroy() {

    if (this.parent) {
      this.parent.children.splice(this.parent.children.indexOf(this), 1);
      this.parent.folders.splice(this.parent.folders.indexOf(this), 1);
    }

    if (this.domElement.parentElement) {
      this.domElement.parentElement.removeChild(this.domElement);
    }

    Array.from(this.children).forEach(c => c.destroy());

  }

  /**
   * Returns an array of controllers contained by this GUI and its descendents.
   * @returns {Controller[]}
   */
  controllersRecursive() {
    let controllers = Array.from(this.controllers);
    this.folders.forEach(f => {
      controllers = controllers.concat(f.controllersRecursive());
    });
    return controllers;
  }

  /**
   * Returns an array of folders contained by this GUI and its descendents.
   * @returns {GUI[]}
   */
  foldersRecursive() {
    let folders = Array.from(this.folders);
    this.folders.forEach(f => {
      folders = folders.concat(f.foldersRecursive());
    });
    return folders;
  }
  _render(key_, value) {
    const data =  this.UC.data
    var key = `${this.PATH}${key_}`
    data[key] = value
    this.UC.setData(data)
  }
  _findData(find_path) {
    var array = find_path.split(".")
    var data = this.UC.data
    for (const item of array) {
      var key, index
      var p = item.indexOf("[")
      var q = item.indexOf("]")
      if (q >= 0) {
        var key = item.substr(0, p)
        var index = item.substring(p + 1, q)
        data = data[key][index]
      } else {
        data = data[item]
      }
    }
    return data
  }
  _findNode(find_path) {
    var array = find_path.split(".")
    var data = this
    for (const item of array) {
      var key, index
      var p = item.indexOf("[")
      var q = item.indexOf("]")

      var index = item.substring(p + 1, q)
      data = data.children[index]
    }
    return data
  }
}